using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using System.Xml;

namespace Woz
{
    /// <summary>
    /// the screen manager class manages a master list of all
    /// of the screens
    /// </summary>
    public class ScreenManager
    {
        private static readonly byte[] sWoz10Signature;
        static ScreenManager()
        {
            sWoz10Signature = new byte[] {
                (byte)'W',
                (byte)'o',
                (byte)'z',
                (byte)'1',
                (byte)'0'
            };
        }

        /// <summary>
        /// private constructor of the screen manager
        /// </summary>
        private ScreenManager()
        {
            // create the list of screens
            this.mScreens = new List<Woz.Screen>();

            // create the list of transition arrows
            this.mTransitionArrows = new List<Woz.TransitionArrow>();

            // add a blank screen as the base screen
            this.mSelectedScreen = this.AddNewScreen("", false);
        }

        /// <summary>
        /// get an instance of the screen manager
        /// </summary>
        public static ScreenManager Instance
        {
            get
            {
                // if an instance doesnt already exist create it
                if (sInstance == null)
                {
                    sInstance = new ScreenManager();
                }

                return sInstance;
            }
        }

        /// <summary>
        /// gets and sets the currently selected screen
        /// </summary>
        public Woz.Screen SelectedScreen
        {
            get
            {
                return this.mSelectedScreen;
            }
            set
            {
                // make sure have a valid value
                if ((this.mSelectedScreen == value) || (value == null))
                {
                    return;
                }

                // this new value is not the same as the current nor is it null so set it
                this.mSelectedScreen = value;

                // fire the screen selected event
                if (this.ScreenSelected != null)
                {
                    this.ScreenSelected.Invoke(this, this.mSelectedScreen);
                }
            }
        }

        /// <summary>
        /// get a list of all the screens 
        /// </summary>
        public List<Woz.Screen> Screens
        {
            get
            {
                return this.mScreens;
            }
        }

        /// <summary>
        /// get a list of all the screens 
        /// </summary>
        public List<Woz.TransitionArrow> TransitionArrows
        {
            get
            {
                return this.mTransitionArrows;
            }
        }

        /// <summary>
        /// gets the copied entities
        /// </summary>
        public Woz.Entity[] CopiedEntities
        {
            get
            {
                return this.mCopiedEntities;
            }
        }

        /// <summary>
        /// returns a screen with the given screen name
        /// </summary>
        /// <param name="pScreenName">the name of the screen to retrieve</param>
        /// <returns>the screen whose name is given or null if not found</returns>
        public Woz.Screen GetScreen(string pScreenName)
        {
            foreach (Woz.Screen screen in this.mScreens)
            {
                if (screen.Name == pScreenName)
                {
                    return screen;
                }
            }
            
            return null;
        }

        /// <summary>
        /// add a transition arrow between two screen thumbnails and returns the transition arrow that was just added
        /// </summary>
        /// <param name="pStart">the starting screen thumbnail</param>
        /// <param name="pEnd">the ending screen thumbnail</param>
        /// <returns>the transition arrow that was just added</returns>
        public Woz.TransitionArrow AddScreenTransition(Woz.ScreenThumbnail pStart, Woz.ScreenThumbnail pEnd)
        {
            // create the transition
            Woz.TransitionArrow transition = new Woz.TransitionArrow(pStart, pEnd);

            // add the transition to the list of transitions
            this.mTransitionArrows.Add(transition);

            // remove the next screens
            pStart.Screen.AddNextScreen(pEnd.Screen);

            if (this.ScreenTransitionAdded != null)
            {
                this.ScreenTransitionAdded.Invoke(transition, EventArgs.Empty);
            }

            // return the transition
            return transition;
        }

        /// <summary>
        /// remove the given transition arrow
        /// </summary>
        /// <param name="pTransitionArrow">the transition arrow to remove</param>
        public void RemoveScreenTransition(Woz.TransitionArrow pTransitionArrow)
        {
            if (pTransitionArrow == null)
            {
                return;
            }

            // remove the transition arrow
            this.mTransitionArrows.Remove(pTransitionArrow);

            // remove the next screens
            pTransitionArrow.StartThumbnail.Screen.RemoveNextScreen(pTransitionArrow.EndThumbnail.Screen);

            if (this.ScreenTransitionRemoved != null)
            {
                this.ScreenTransitionRemoved.Invoke(pTransitionArrow, EventArgs.Empty);
            }
        }

        /// <summary>
        /// add a new screen to the list of screens and return it
        /// </summary>
        /// <param name="pScreenName">name of the new screen or pass in empty string for default naming</param>
        /// <returns>a blank screen</returns>
        public Woz.Screen AddNewScreen(string pScreenName, bool pHidden)
        {
            // check if the screen name is empty
            if (pScreenName == "")
            {
                pScreenName = Woz.StaticVariables.Instance.DefaultScreenName;
            }

            // create the new screen with the next available version of this name
            Woz.Screen newScreen = new Woz.Screen(this.GetNextAvailableName(pScreenName), pHidden);

            // add the new screen to the list of screens
            this.mScreens.Add(newScreen);

            // fire the screen added event
            if (this.ScreenAdded != null)
            {
                this.ScreenAdded.Invoke(this, newScreen);
            }

            // return the new screen
            return newScreen;
        }

        public void AddExistingScreen(Woz.Screen pScreen)
        {
            // make sure this screen does not already exist in our list
            if (this.mScreens.Contains(pScreen))
            {
                return;
            }

            // also make sure this name is valid
            pScreen.Name = this.GetNextAvailableName(pScreen.Name);

            // add this screen to our list of screens
            this.mScreens.Add(pScreen);
        }

        /// <summary>
        /// clone the given screen and add it to our list return the clone
        /// </summary>
        /// <param name="pScreen">the screen to clone</param>
        /// <returns>a new screen identical to pScreen</returns>
        public Woz.Screen CloneScreen(Woz.Screen pScreen)
        {
            // create the new screen
            Woz.Screen newScreen = pScreen.Clone(this.GetNextAvailableName(pScreen.Name));

            // add the new screen to the list of screens
            this.mScreens.Add(newScreen);

            // fire the screen added event
            if (this.ScreenAdded != null)
            {
                this.ScreenAdded.Invoke(this, newScreen);
            }

            // return the new screen
            return newScreen;
        }

        /// <summary>
        /// deletes the selected screen and sets the selected screen to the next
        /// screen in the list or previous if the selected screen is the last
        /// screen in the list or a blank screen if this is the only 
        /// screen in the lisst
        /// </summary>
        public void DeleteSelectedScreen()
        {
            // save a reference to the current screen
            Woz.Screen screenToRemove = this.mSelectedScreen;

            // find the current screen position
            int currentScreenPosition = this.mScreens.IndexOf(this.mSelectedScreen);

            // ignore if no such screen exists
            if (currentScreenPosition == -1)
            {
                return;
            }

            // connect the previous screen to the current screen
            if (currentScreenPosition > 0)
            {
                // get a reference to the previous screen
                Woz.Screen prevScreen = this.mScreens[currentScreenPosition - 1];
            }

            // find the closest next screen thats not hidden
            for (int i = currentScreenPosition + 1; i < this.mScreens.Count; i++)
            {
                if (this.mScreens[i].Hidden == false)
                {
                    // found it delete the current screen and assign it to this screen
                    this.SelectedScreen = this.mScreens[i];
                    this.mScreens.RemoveAt(currentScreenPosition);
                    
                    // fire the screen removed event
                    if (this.ScreenRemoved != null)
                    {
                        this.ScreenRemoved.Invoke(this, screenToRemove);
                    }

                    // we are done
                    return;
                }
            }

            // find the closest previous screen thats not hidden
            for (int i = currentScreenPosition - 1; i >= 0; i--)
            {
                if (this.mScreens[i].Hidden == false)
                {
                    // found it delete the current screen and assign it to this screen
                    this.SelectedScreen = this.mScreens[i];
                    this.mScreens.RemoveAt(currentScreenPosition);

                    // fire the screen removed event
                    if (this.ScreenRemoved != null)
                    {
                        this.ScreenRemoved.Invoke(this, screenToRemove);
                    }

                    // we are done
                    return;
                }
            }

            // if we made if this far then there is no more screens left
            // but we need atleast one screen at all times so create a new one
            this.SelectedScreen = this.AddNewScreen(this.GetNextAvailableName(""), false);
            this.mScreens.RemoveAt(currentScreenPosition);

            // fire the screen removed event
            if (this.ScreenRemoved != null)
            {
                this.ScreenRemoved.Invoke(this, screenToRemove);
            }
        }

        /// <summary>
        /// calculates the next available name in sequence and returns it
        /// </summary>
        /// <param name="pBaseName">the base name to start with</param>
        /// <returns>a unique calculated name</returns>
        public string GetNextAvailableName(string pBaseName)
        {
            // we use i later
            int i, availableNumber = 1;

            // get the list of names
            List<string> listOfNames = new List<string>(this.mScreens.Count);
            for (i = 0; i < this.mScreens.Count; i++)
            {
                listOfNames.Add(this.mScreens[i].Name);
            }

            // check if this name is already in use
            if (listOfNames.Contains(pBaseName))
            {
                // now get the numeric part of the name if any
                for (i = pBaseName.Length - 1; i >= 0; i--)
                {
                    // keep going through the string until we find a non number
                    if (!char.IsNumber(pBaseName, i))
                    {
                        // and get out of the loop
                        break;
                    }
                }

                // check if the whole string is a number
                if (i < 0)
                {
                    // the available number is then the whole base name
                    availableNumber = int.Parse(pBaseName);

                    // make sure this name is not already in the list
                    while(listOfNames.Contains(availableNumber.ToString()))
                    {
                        availableNumber++;
                    }

                    // set the base name to empty string
                    pBaseName = "";
                }
                else
                {
                    // get the numeric part of base name
                    string numericSuffix = pBaseName.Substring(i + 1);

                    // set the available number to it
                    if (int.TryParse(numericSuffix, out availableNumber))
                    {
                        // set the base name to everything before the numeric suffix
                        pBaseName = pBaseName.Substring(0, i + 1);

                        // now find the next available name
                        while (listOfNames.Contains(String.Concat(pBaseName, availableNumber)))
                        {
                            availableNumber++;
                        }
                    }

                    // if this name is available then add one and try again
                    else if (listOfNames.Contains(pBaseName))
                    {
                        return this.GetNextAvailableName(String.Format("{0} 1", pBaseName.TrimEnd()));
                    }

                    // lastly this name is not available so just return it
                    else
                    {
                        return pBaseName;
                    }
                }

                // this is the next available screen name
                return string.Concat(pBaseName, availableNumber);
            }

            return pBaseName;
        }

        /// <summary>
        /// add an entity to the selected screen
        /// </summary>
        /// <param name="pEntity">the entity to add</param>
        public void AddEntityToSelectedScreen(Woz.Entity pEntity)
        {
            // add the entity to the current screen
            this.mSelectedScreen.AddEntity(pEntity, true);

            // fire the entity added event
            if (this.SelectedScreenEntityAdded != null)
            {
                this.SelectedScreenEntityAdded.Invoke(this, pEntity.BoundingBox);
            }

            // fire the changed event
            if (this.SelectedScreenChanged != null)
            {
                this.SelectedScreenChanged.Invoke(this, pEntity.BoundingBox);
            }
        }

        /// <summary>
        /// remove an entity from the selected screen
        /// </summary>
        /// <param name="pEntity">the entity to remove</param>
        public void RemoveEntityFromSelectedScreen(Woz.Entity pEntity)
        {
            // remove the entity from the current screen
            this.mSelectedScreen.RemoveEntity(pEntity, true);

            // fire the entity removed event
            if (this.SelectedScreenEntityRemoved != null)
            {
                this.SelectedScreenEntityRemoved.Invoke(this, pEntity.BoundingBox);
            }

            // fire the changed event
            if (this.SelectedScreenChanged != null)
            {
                this.SelectedScreenChanged.Invoke(this, pEntity.BoundingBox);
            }
        }

        /// <summary>
        /// draw the selected screen using the given graphics object
        /// </summary>
        /// <param name="pGraphics">the graphics object to draw with</param>
        public void DrawSelectedScreen(Graphics pGraphics)
        {
            // draw the current screen
            if (this.mSelectedScreen != null)
            {
                this.mSelectedScreen.Draw(pGraphics);
            }
        }

        /// <summary>
        /// clear the selected screen
        /// </summary>
        public void ClearSelectedScreen()
        {
            // clear the current screen
            this.mSelectedScreen.Clear();

            // fire the cleared event
            if (this.SelectedScreenCleared != null)
            {
                this.SelectedScreenCleared.Invoke(this, Woz.InkSpaceConverter.Instance.InkSpaceCanvasRectangle);
            }

            // fire the changed event
            if (this.SelectedScreenChanged != null)
            {
                this.SelectedScreenChanged.Invoke(this, Woz.InkSpaceConverter.Instance.InkSpaceCanvasRectangle);
            }
        }

        /// <summary>
        /// add a selected entity to the selected screen
        /// </summary>
        /// <param name="pEntity">the selected entity to add</param>
        public void AddSelectedEntityToSelectedScreen(Woz.Entity pEntity)
        {
            // add the entity to the selected screen
            this.mSelectedScreen.SelectEntity(pEntity, true);

            // fire the changed event
            if (this.SelectedScreenChanged != null)
            {
                // inflate the bounding rectangle by about ten pixels
                Rectangle boundingRect = pEntity.BoundingBox;
                boundingRect.Inflate(Woz.InkSpaceConverter.Instance.InkSpaceTenPixels,
                    Woz.InkSpaceConverter.Instance.InkSpaceTenPixels);

                // fire the changed event with the inflated bounding box
                this.SelectedScreenChanged.Invoke(this, boundingRect);
            }
        }

        /// <summary>
        /// remove a selected entity from the selected screen
        /// </summary>
        /// <param name="pEntity">the selected entity to remove from the selected screen</param>
        public void RemoveSelectedEntityToSelectedScreen(Woz.Entity pEntity)
        {
            // remove the selected entity from the selected screen
            this.mSelectedScreen.DeselectEntity(pEntity, true);

            // fire the changed event
            if (this.SelectedScreenChanged != null)
            {
                // inflate the bounding rectangle by about ten pixels
                Rectangle boundingRect = pEntity.BoundingBox;
                boundingRect.Inflate(Woz.InkSpaceConverter.Instance.InkSpaceTenPixels,
                    Woz.InkSpaceConverter.Instance.InkSpaceTenPixels);

                // fire the changed event with the inflated bounding box
                this.SelectedScreenChanged.Invoke(this, boundingRect);
            }
        }

        /// <summary>
        /// requests that the canvas update a portion of the selected screen
        /// </summary>
        /// <param name="pScreen">the screen to check against</param>
        /// <param name="pBoundingBox">the portion of the selected screen to update</param>
        public void RequestSelectedScreenUpdate(Woz.Screen pScreen, Rectangle pBoundingBox)
        {
            // update the selected entities bounding box
            pScreen.UpdateSelectedEntitiesBoundingBox();

            // make sure the screen is infact the selected screen
            if (pScreen == this.mSelectedScreen)
            {
                // fire the changed event
                if (this.SelectedScreenUpdateRequested != null)
                {
                    this.SelectedScreenUpdateRequested.Invoke(this, pBoundingBox);
                }
            }
        }

        /// <summary>
        /// request that the thumbnail gets updated
        /// </summary>
        /// <param name="pScreen">the screen to check against</param>
        public void RequestSelectedScreenThumbnailUpdate(Woz.Screen pScreen)
        {
            // just call the screens update thumbnail method
            pScreen.UpdateThumbnail(Woz.Screen.UpdateMode.Remove, null);

            // make sure the screen is infact the selected screen
            if (pScreen == this.mSelectedScreen)
            {
                // now notify the whole world that a thumbnail update has been requested
                if (this.SelectedScreenThumbnailUpdateRequested != null)
                {
                    this.SelectedScreenThumbnailUpdateRequested.Invoke(this, Rectangle.Empty);
                }
            }
        }

        /// <summary>
        /// copies the selected entities to the internal clipboard
        /// </summary>
        public void CopySelectedEntities()
        {
            // save the selected entities
            this.mCopiedEntities = this.mSelectedScreen.SelectedEntities;
        }

        /// <summary>
        /// saves the current project with the given filename
        /// </summary>
        /// <param name="pFilename">the woz file to save to</param>
        public void SaveProject(string pFilename)
        {
            // create a temporary path to put the file together
            string path = String.Concat(Path.GetTempPath(), Path.GetRandomFileName());
            Directory.CreateDirectory(path);

            Console.WriteLine("PackPath: {0}", path);
            {
                // start the progress bar
                Woz.ResourceManager.Instance.ProgressStartMethod.Invoke(String.Concat("Saving project: ", Path.GetFileName(pFilename)), null);

                // compute the total percentage and amount to increment by
                float totalPercent = 0;
                float incAmount = 70.0f / ((float)this.mScreens.Count * 3);

                // create the xml files
                string screensFile = Path.Combine(path, "Screens.xml");
                XmlWriter screensTW = Woz.Utilities.Instance.CreateXmlWriter(screensFile, "Screens");

                string entitiesFile = Path.Combine(path, "Entities.xml");
                XmlWriter entitiesTW = Woz.Utilities.Instance.CreateXmlWriter(entitiesFile, "Entities");

                string transitionsFile = Path.Combine(path, "Transitions.xml");
                XmlWriter transitionsTW = Woz.Utilities.Instance.CreateXmlWriter(transitionsFile, "Transitions");

                // used to maintain a unique list of entities
                List<Woz.Entity> uniqueEntities = new List<Woz.Entity>();

                // go through all of the screens
                foreach (Woz.Screen screen in this.mScreens)
                {
                    // save the screen
                    screen.Save(screensTW);

                    // show progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke((int)(totalPercent += incAmount));

                    // save its transitions
                    {
                        transitionsTW.WriteStartElement("Screen", "urn:Woz");
                        transitionsTW.WriteAttributeString("ID", screen.ID.ToString());
                        transitionsTW.WriteStartElement("NextScreens");

                        // go through all of the next screens
                        foreach (Woz.Screen nextScreen in screen.NextScreens)
                        {
                            transitionsTW.WriteStartElement("Screen", "urn:Woz");
                            transitionsTW.WriteAttributeString("ID", nextScreen.ID.ToString());
                            transitionsTW.WriteEndElement();
                        }

                        transitionsTW.WriteEndElement();
                        transitionsTW.WriteEndElement();
                    }

                    // show progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke((int)(totalPercent += incAmount));

                    // save its entities
                    this.SaveEntities(screen.Entities, uniqueEntities, entitiesTW, path);

                    // show progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke((int)(totalPercent += incAmount));
                }

                // wrap them all up
                screensTW.WriteEndDocument();
                entitiesTW.WriteEndDocument();
                transitionsTW.WriteEndDocument();

                // close them all up
                screensTW.Close();
                entitiesTW.Close();
                transitionsTW.Close();

                // create the extras file
                {
                    string extrasFile = Path.Combine(path, "Extras.xml");
                    XmlWriter extrasTW = Woz.Utilities.Instance.CreateXmlWriter(extrasFile, "Extras");

                    // write the transition thumbnails
                    extrasTW.WriteStartElement("TransitionThumbnails");
                    foreach (Woz.Screen screen in this.mScreens)
                    {
                        extrasTW.WriteStartElement("Thumbnail");
                        extrasTW.WriteAttributeString("ID", screen.ID.ToString());
                        extrasTW.WriteAttributeString("LinearIndex", screen.SliderThumbnail.TabIndex.ToString());
                        extrasTW.WriteAttributeString("CustomIndex", screen.TransitionsThumbnail.TabIndex.ToString());
                        extrasTW.WriteAttributeString("Unused", (screen.TransitionsThumbnail.Tag == null) ? "Yes" : "");
                        extrasTW.WriteAttributeString("x", screen.TransitionsThumbnail.Left.ToString());
                        extrasTW.WriteAttributeString("y", screen.TransitionsThumbnail.Top.ToString());
                        extrasTW.WriteEndElement();
                    }
                    extrasTW.WriteEndElement();

                    // show progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(80);

                    // write the stretch positions
                    extrasTW.WriteStartElement("StretchPositions");
                    foreach (Woz.TransitionArrow transition in this.mTransitionArrows)
                    {
                        extrasTW.WriteStartElement("Position");
                        extrasTW.WriteAttributeString("StartID", transition.StartThumbnail.Screen.ID.ToString());
                        extrasTW.WriteAttributeString("EndID", transition.EndThumbnail.Screen.ID.ToString());
                        extrasTW.WriteAttributeString("x", transition.StretchPosition.X.ToString());
                        extrasTW.WriteAttributeString("y", transition.StretchPosition.Y.ToString());
                        extrasTW.WriteEndElement();
                    }
                    extrasTW.WriteEndElement();

                    // close the document
                    extrasTW.WriteEndDocument();
                    extrasTW.Close();
                }

                // show progress
                Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(85);

                // create the settings file
                {
                    string settingsFile = Path.Combine(path, "Settings.xml");
                    XmlWriter settingsTW = Woz.Utilities.Instance.CreateXmlWriter(settingsFile, "Settings");

                    // save whether were using custome transitions
                    settingsTW.WriteStartElement("Item");
                    settingsTW.WriteElementString("Name", "UsingCustomTransitions");
                    settingsTW.WriteElementString("Value", Woz.DynamicVariables.Instance.UsingCustomTransitions.ToString());
                    settingsTW.WriteEndElement();

                    // close the document
                    settingsTW.WriteEndDocument();
                    settingsTW.Close();
                }
            }

            // now compress all of the files together
            string[] files = Directory.GetFiles(path);

            // pack all of the files
            byte[] packedData = Woz.Utilities.Instance.PackFiles(files);

            // save the pack file
            #if DEBUG
            string uncompressedPackFile = String.Concat(path, "\\uncompressed.pack");
            using (BinaryWriter sw = new BinaryWriter(File.Create(uncompressedPackFile)))
            {
                // pack all of the files and write to the uncompressed pack file
                sw.Write(packedData);
            }
            #endif

            // show progress
            Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(90);

            // create the final file
            using (BinaryWriter sw = new BinaryWriter(File.Create(pFilename)))
            {
                // first write woz signature
                char[] wozSignature = new char[] { 'W', 'o', 'z', '1', '0' };
                sw.Write(wozSignature);

                // lzma compress the pack
                byte[] compressed = SevenZip.Compression.LZMA.SevenZipHelper.Compress(packedData);

                // write lzma compressed pack
                sw.Write(compressed.Length);
                sw.Write(compressed);
            }

            #if !DEBUG
            // delete all of the generated files
            foreach (string file in files)
            {
                File.Delete(file);
            }

            // delete the temp directory
            Directory.Delete(path);
            #endif

            // show progress
            Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(100);

            // end progress
            Woz.ResourceManager.Instance.ProgressEndMethod.Invoke(3);

            // force garbage collection
            GC.Collect();
        }

        /// <summary>
        /// helper function which recursively saves all entities passed to it in a flat list
        /// </summary>
        /// <param name="entities">the list of entities to save</param>
        /// <param name="uniqueEntities">list of unique entities</param>
        /// <param name="entitiesTW">the xml stream to save to</param>
        /// <param name="path">the path to the entities file</param>
        private void SaveEntities(Woz.Entity[] entities, List<Woz.Entity> uniqueEntities, XmlWriter entitiesTW, string path)
        {
            // go through all the entities passed in
            foreach (Woz.Entity entity in entities)
            {
                // make sure the entity has not been saved before
                if (!uniqueEntities.Contains(entity))
                {
                    // add it to the unique entities list
                    uniqueEntities.Add(entity);

                    // see if this entity is a container and handle it accordingly
                    Woz.IEntityContainer container = entity as Woz.IEntityContainer;
                    if (container != null)
                    {
                        // save all the entities in the container
                        this.SaveEntities(container.Entities, uniqueEntities, entitiesTW, path);
                    }

                    // save the entity
                    entity.Save(entitiesTW, path);
                }
            }
        }

        /// <summary>
        /// preloads a woz file and checks whether it is a valid file or not
        /// </summary>
        /// <param name="pFilename">the file to load</param>
        /// <returns>true if the file is valid or false if not</returns>
        public bool PreLoadProject(string pFilename)
        {
            // indicates whether an entity was skipped or not
            bool entitySkipped = false;

            // reset loaded screens
            this.mLoadedScreens = null;

            // first check if its a .woz file
            if (!Path.GetExtension(pFilename).Equals(".woz", StringComparison.CurrentCultureIgnoreCase))
            {
                MessageBox.Show(String.Format("Sorry, {0} is not a valid WOZ Pro document.", Path.GetFileName(pFilename)),
                    "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return false;
            }

            // now check woz version
            byte[] wozSignature = new byte[5];
            using (BinaryReader br = new BinaryReader(File.OpenRead(pFilename)))
            {
                // read the signature
                wozSignature = br.ReadBytes(5);

                // now load the proper version
                if (this.CompareSig(wozSignature, sWoz10Signature))
                {
                    // start the progress
                    Woz.ResourceManager.Instance.ProgressStartMethod.Invoke(String.Concat("Loading project: ", Path.GetFileName(pFilename)), null);

                    // get a temp directory and create it
                    string path = String.Concat(Path.GetTempPath(), Path.GetRandomFileName());
                    Directory.CreateDirectory(path);

                    // get the size of the compressed stream
                    int compressedSize = br.ReadInt32();

                    // read the compressed data
                    byte[] compressedData = Woz.Utilities.Instance.ReadStream(br.BaseStream, compressedSize);

                    // uncompress the compressed data
                    byte[] uncompressedData = SevenZip.Compression.LZMA.SevenZipHelper.Decompress(compressedData);

#if DEBUG
                    // write the uncompressed data to disk
                    using (BinaryWriter bw = new BinaryWriter(File.Create(Path.Combine(path, "uncompressed.pack"))))
                    {
                        bw.Write(uncompressedData);
                    }
#endif

                    Console.WriteLine("UnpackPath: {0}", path);

                    // update the progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(10);

                    // get the list of files
                    string[] files = Woz.Utilities.Instance.UnPackFiles(uncompressedData, path);

                    // load the entities
                    this.mLoadedEntities = new Dictionary<uint, Woz.Entity>();
                    using (XmlReader reader = XmlReader.Create(Path.Combine(path, "Entities.xml")))
                    {
                        // get a list of all entity types
                        Dictionary<string, Type> entityTypes = new Dictionary<string, Type>();
                        {
                            string searchPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
                            string[] assemblyFiles = Directory.GetFiles(searchPath, "*.dll");
                            foreach (string file in assemblyFiles)
                            {
                                System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFrom(file);
                                foreach (Type type in assembly.GetTypes())
                                {
                                    if (type.BaseType == typeof(Woz.Entity))
                                    {
                                        entityTypes.Add(type.FullName, type);
                                    }
                                }
                            }
                        }

                        // update the progress
                        Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(25);

                        reader.ReadStartElement("Entities");
                        while (reader.IsStartElement("Entity", "urn:Woz"))
                        {
                            uint id = uint.Parse(reader.GetAttribute("ID"));
                            reader.ReadStartElement("Entity", "urn:Woz");
                            {
                                int top = int.Parse(reader.ReadElementString("Top"));
                                int left = int.Parse(reader.ReadElementString("Left"));
                                int width = int.Parse(reader.ReadElementString("Width"));
                                int height = int.Parse(reader.ReadElementString("Height"));
                                bool locked = bool.Parse(reader.ReadElementString("Locked"));
                                string type = reader.ReadElementString("Type");

                                // add the entity to the list of entities
                                if (entityTypes.ContainsKey(type))
                                {
                                    Woz.Entity entity = Activator.CreateInstance(entityTypes[type], reader, new Rectangle(left, top, width, height), path) as Woz.Entity;
                                    entity.Locked = locked;
                                    this.mLoadedEntities.Add(id, entity);
                                }
                                else
                                {
                                    entitySkipped = true;
                                }
                            }
                            reader.ReadEndElement();
                        }
                        reader.ReadEndElement();

                        // update the progress
                        Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(45);
                    }

                    // load the screens
                    Dictionary<uint, Woz.Screen> loadedScreens = new Dictionary<uint, Woz.Screen>();
                    using (XmlReader reader = XmlReader.Create(Path.Combine(path, "Screens.xml")))
                    {
                        reader.ReadStartElement("Screens");
                        while (reader.IsStartElement("Screen", "urn:Woz"))
                        {
                            uint id = uint.Parse(reader.GetAttribute("ID"));
                            reader.ReadStartElement("Screen", "urn:Woz");
                            {
                                // read the name and hidden tags
                                string name = reader.ReadElementString("Name");
                                bool hidden = bool.Parse(reader.ReadElementString("Hidden"));

                                // create the screen
                                Woz.Screen screen = new Woz.Screen(name, hidden);

                                // add the screen to the list of screens
                                loadedScreens.Add(id, screen);

                                // add entities
                                reader.ReadStartElement("Entities");
                                if (reader.IsStartElement("Entity", "urn:Woz"))
                                {
                                    while (reader.IsStartElement("Entity", "urn:Woz"))
                                    {
                                        uint entityID = uint.Parse(reader.GetAttribute("ID"));
                                        if (this.mLoadedEntities.ContainsKey(entityID))
                                        {
                                            screen.AddEntity(this.mLoadedEntities[entityID], false);
                                            reader.ReadElementString("Entity", "urn:Woz");
                                        }
                                    }
                                    reader.ReadEndElement();
                                }
                            }
                            reader.ReadEndElement();
                        }
                        reader.ReadEndElement();
                    }

                    // were done with the list of loaded entities so clear it
                    this.mLoadedEntities.Clear();
                    this.mLoadedEntities = null;

                    // update the progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(65);

                    // load the transitions
                    Dictionary<string, Woz.TransitionArrow> loadedTransitionArrows = new Dictionary<string, Woz.TransitionArrow>();
                    using (XmlReader reader = XmlReader.Create(Path.Combine(path, "Transitions.xml")))
                    {
                        reader.ReadStartElement("Transitions");
                            while (reader.IsStartElement("Screen", "urn:Woz"))
                            {
                                uint screenID = uint.Parse(reader.GetAttribute("ID"));
                                reader.ReadStartElement("Screen", "urn:Woz");
                                {
                                    reader.ReadStartElement("NextScreens");
                                    if (reader.IsStartElement("Screen", "urn:Woz"))
                                    {
                                        while (reader.IsStartElement("Screen", "urn:Woz"))
                                        {
                                            uint nextScreenID = uint.Parse(reader.GetAttribute("ID"));
                                            if (loadedScreens.ContainsKey(screenID) && loadedScreens.ContainsKey(nextScreenID))
                                            {
                                                Woz.TransitionArrow transition = new Woz.TransitionArrow(loadedScreens[screenID].TransitionsThumbnail, 
                                                    loadedScreens[nextScreenID].TransitionsThumbnail);
                                                loadedTransitionArrows.Add(String.Format("{0}:{1}", screenID, nextScreenID), transition);
                                            }
                                            reader.ReadElementString("Screen", "urn:Woz");
                                        }
                                        reader.ReadEndElement();
                                    }
                                }
                                reader.ReadEndElement();
                            }
                            reader.ReadEndElement();
                    }

                    // update the progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(75);

                    // load the extras
                    if (File.Exists(Path.Combine(path, "Extras.xml")))
                    {
                        using (XmlReader reader = XmlReader.Create(Path.Combine(path, "Extras.xml")))
                        {
                            reader.ReadStartElement("Extras");
                            while (reader.Read())
                            {
                                if (reader.IsStartElement("TransitionThumbnails"))
                                {
                                    reader.ReadStartElement("TransitionThumbnails");
                                    while (reader.IsStartElement("Thumbnail"))
                                    {
                                        uint screenID = uint.Parse(reader.GetAttribute("ID"));
                                        if(loadedScreens.ContainsKey(screenID))
                                        {
                                            loadedScreens[screenID].SliderThumbnail.TabIndex = int.Parse(reader.GetAttribute("LinearIndex"));
                                            loadedScreens[screenID].TransitionsThumbnail.TabIndex = int.Parse(reader.GetAttribute("CustomIndex"));
                                            loadedScreens[screenID].TransitionsThumbnail.Tag = (reader.GetAttribute("Unused") == "Yes") ? null : "IN-USE";
                                            loadedScreens[screenID].TransitionsThumbnail.Location = new Point(
                                                int.Parse(reader.GetAttribute("x")), 
                                                int.Parse(reader.GetAttribute("y")));
                                        }
                                        reader.ReadElementString();
                                    }
                                    reader.ReadEndElement();
                                }
                                else if(reader.IsStartElement("StretchPositions"))
                                {
                                    reader.ReadStartElement("StretchPositions");
                                    while(reader.IsStartElement("Position"))
                                    {
                                        uint startScreenID = uint.Parse(reader.GetAttribute("StartID"));
                                        uint endScreenID = uint.Parse(reader.GetAttribute("EndID"));
                                        if(loadedScreens.ContainsKey(startScreenID) && loadedScreens.ContainsKey(endScreenID))
                                        {
                                            string transitionArrowID = string.Format("{0}:{1}", startScreenID, endScreenID);
                                            if (loadedTransitionArrows.ContainsKey(transitionArrowID))
                                            {
                                                Point stretchPos = new Point(int.Parse(reader.GetAttribute("x")), int.Parse(reader.GetAttribute("y")));
                                                loadedTransitionArrows[transitionArrowID].StrecthTo(stretchPos);
                                            }
                                        }
                                        reader.ReadElementString();
                                    }
                                    reader.ReadEndElement();
                                }
                            }
                        }
                    }

                    // update the progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(85);

                    // load the extras
                    if (File.Exists(Path.Combine(path, "Settings.xml")))
                    {
                        using (XmlReader reader = XmlReader.Create(Path.Combine(path, "Settings.xml")))
                        {
                            reader.ReadStartElement("Settings");
                            while (reader.IsStartElement("Item"))
                            {
                                reader.ReadStartElement("Item");
                                string name = reader.ReadElementString("Name");
                                string value = reader.ReadElementString("Value");
                                if (this.mLoadedSettings == null)
                                {
                                    this.mLoadedSettings = new Dictionary<string, string>();
                                }
                                this.mLoadedSettings.Add(name, value);
                                reader.ReadEndElement();
                            }
                            reader.ReadEndElement();
                        }
                    }

                    // save the loaded screens for later
                    this.mLoadedScreens = new Woz.Screen[loadedScreens.Count];
                    loadedScreens.Values.CopyTo(this.mLoadedScreens, 0);

                    // save loaded transitions for later
                    this.mLoadedTransitions = new Woz.TransitionArrow[loadedTransitionArrows.Count];
                    loadedTransitionArrows.Values.CopyTo(this.mLoadedTransitions, 0);

                    // show a message box if an entity has been skipped
                    if (entitySkipped)
                    {
                        MessageBox.Show("One or more entities have been skipped because the tool that created them can not be found.\n\nPress OK to continue...",
                            "Entity Skipped", MessageBoxButtons.OK, MessageBoxIcon.Warning);
                    }

                    // update the progress
                    Woz.ResourceManager.Instance.ProgressUpdateMethod.Invoke(100);

                    // done loading
                    Woz.ResourceManager.Instance.ProgressEndMethod.Invoke(3);

                    // success
                    return true;
                }

                // if all else fails
                else
                {
                    MessageBox.Show("Sorry, this is an unsupported version of WOZ Pro, please upgrade to the latest version.",
                        "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return false;
                }
            }
        }

        /// <summary>
        /// makes sure that it can load the given woz file and then loads it
        /// </summary>
        public void FinishLoadingProject()
        {
            if (this.mLoadedScreens == null)
            {
                return;
            }

            // load the screens
            foreach (Woz.Screen screen in this.mLoadedScreens)
            {
                // add this screen to the list of screens
                this.AddExistingScreen(screen);
                screen.UpdateThumbnail(Woz.Screen.UpdateMode.Remove, null);
                
                // fire the screen added event
                if (this.ScreenAdded != null)
                {
                    this.ScreenAdded.Invoke(this, screen);
                }
            }

            // load the transitions
            foreach (Woz.TransitionArrow transition in this.mLoadedTransitions)
            {
                // add it to the transition arrows list
                this.mTransitionArrows.Add(transition);

                // add a link between the two screens
                transition.StartThumbnail.Screen.AddNextScreen(transition.EndThumbnail.Screen);

                // fire the transition added event
                if (this.ScreenTransitionAdded != null)
                {
                    this.ScreenTransitionAdded.Invoke(this, EventArgs.Empty);
                }
            }

            // load the settings
            if (this.mLoadedSettings != null)
            {
                if (this.mLoadedSettings.ContainsKey("UsingCustomTransitions"))
                {
                    Woz.DynamicVariables.Instance.UsingCustomTransitions = bool.Parse(this.mLoadedSettings["UsingCustomTransitions"].ToLower());
                }
            }

            // set the selected screen
            this.SelectedScreen = this.mLoadedScreens[0];

            // clear the loaded screens
            this.mLoadedScreens = null;

            // clear the loaded settings
            this.mLoadedSettings = null;
        }

        /// <summary>
        /// reset the screen manager
        /// </summary>
        public void Reset()
        {
            // clear the screens
            this.mScreens.Clear();

            // clear the transition arrows
            this.mTransitionArrows.Clear();

            // clear the selected screen
            this.mSelectedScreen = null;
        }

        /// <summary>
        /// finds all of the entities whose id is in the given list of keys
        /// </summary>
        /// <param name="pKeys">the list of keys to search for</param>
        /// <returns>a list of entities whose id is in the keys list</returns>
        public Woz.Entity[] FindLoadedEntities(uint[] pKeys)
        {
            if (this.mLoadedEntities == null)
            {
                return new Woz.Entity[0];
            }

            // load all of the entities corresponding to the keys
            List<Woz.Entity> entities = new List<Woz.Entity>();
            foreach (uint key in pKeys)
            {
                if (this.mLoadedEntities.ContainsKey(key))
                {
                    entities.Add(this.mLoadedEntities[key]);
                }
            }

            // return it
            return entities.ToArray();
        }

        /// <summary>
        /// compares two byte arrays to see if theyre identical
        /// </summary>
        /// <param name="pReadSig">the signature read from file</param>
        /// <param name="pExpectedSig">the expected signature</param>
        /// <returns>true if they are identical</returns>
        private bool CompareSig(byte[] pReadSig, byte[] pExpectedSig)
        {
            if (pReadSig.Length != pExpectedSig.Length)
            {
                return false;
            }
            for (int i = 0; i < pReadSig.Length; i++)
            {
                if (pReadSig[i] != pExpectedSig[i])
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// represents the method that will handle an event that deals with changes to the selected screen
        /// </summary>
        /// <param name="pSender">a reference to the object that fired this event</param>
        /// <param name="pBoundingBox">the bounding box of the area that was changed on the screen</param>
        public delegate void SelectedScreenChangeHandler(object pSender, Rectangle pBoundingBox);

        /// <summary>
        /// represents the method that will handle an event that deals with the screens in the screen manager
        /// </summary>
        /// <param name="pSender">a reference to the object that fired this event</param>
        /// <param name="pScreen">the screen that this method will deal with</param>
        public delegate void ScreenHandler(object pSender, Woz.Screen pScreen);

        /// <summary>
        /// fires when an update is requested for the selected screen
        /// </summary>
        public event SelectedScreenChangeHandler SelectedScreenUpdateRequested;

        /// <summary>
        /// fires when an update is requested for the selected screens thumbnail image
        /// </summary>
        public event SelectedScreenChangeHandler SelectedScreenThumbnailUpdateRequested;

        /// <summary>
        /// fires when the screen contents are changed
        /// </summary>
        public event SelectedScreenChangeHandler SelectedScreenChanged;

        /// <summary>
        /// fires when the contents of the selected screen are cleared
        /// </summary>
        public event SelectedScreenChangeHandler SelectedScreenCleared;

        /// <summary>
        /// fires when an entity is added to the screen
        /// </summary>
        public event SelectedScreenChangeHandler SelectedScreenEntityAdded;

        /// <summary>
        /// fires when an entity is removed from the screen
        /// </summary>
        public event SelectedScreenChangeHandler SelectedScreenEntityRemoved;

        /// <summary>
        /// fires when a screen is added to the list of screens
        /// </summary>
        public event ScreenHandler ScreenAdded;

        /// <summary>
        /// fires when a screen is removed from the list of screens
        /// </summary>
        public event ScreenHandler ScreenRemoved;

        /// <summary>
        /// fires when the selected screen changes
        /// </summary>
        public event ScreenHandler ScreenSelected;

        /// <summary>
        /// occurs when a transition has been added to ourselves
        /// </summary>
        public event EventHandler ScreenTransitionAdded;

        /// <summary>
        /// occurs when a transition has been removed from ourselves
        /// </summary>
        public event EventHandler ScreenTransitionRemoved;

        /// <summary>
        /// the single instance of the screen manager
        /// </summary>
        private static ScreenManager sInstance;

        /// <summary>
        /// list of screens
        /// </summary>
        private List<Woz.Screen> mScreens;

        /// <summary>
        /// list of transition arrows
        /// </summary>
        private List<Woz.TransitionArrow> mTransitionArrows;

        /// <summary>
        /// store a reference to the selected screen
        /// </summary>
        private Woz.Screen mSelectedScreen;

        /// <summary>
        /// the entities that are copied through our internal clipboard
        /// </summary>
        private Woz.Entity[] mCopiedEntities;

        /// <summary>
        /// the list of loaded screens
        /// </summary>
        private Woz.Screen[] mLoadedScreens;

        /// <summary>
        /// the list of loaded transition arrows
        /// </summary>
        private Woz.TransitionArrow[] mLoadedTransitions;

        /// <summary>
        /// the list of loaded entities
        /// </summary>
        private Dictionary<uint, Woz.Entity> mLoadedEntities;

        /// <summary>
        /// the loaded settings
        /// </summary>
        private Dictionary<string, string> mLoadedSettings;
    }
}
